
/*\
 *@! Copyright © 2015 VX Company and Pieter-Bart van Splunter
 *@! Author: Pieter-Bart van Splunter
 *@! Module: SummaryLinkStore 
 *@! Namespace: window.oPB 
 *@! Download: https://summarylinkstore.codeplex.com/
 *@! License: https://summarylinkstore.codeplex.com/license
 *@! Blog: http://www.thesharepoint.nl/pb/javascript/summarylinkstore
\*/

(function SummaryLinkStore_module(window, undefined) {
	//Defining the namespace in a proactive way
	var ns = {};
	if(window.oPB && "slsVersion" in window.oPB)
	{
		(window.oPB.slsConflicted = Object.prototype.toString.call(window.oPB.slsConflicted) === '[object Array]' ? window.oPB.slsConflicted : []).push(ns);
	}
	else
	{
		ns = window.oPB = window.oPB || ns;
	}

	//Public version of oPB.SummaryLinkStore module:
	ns.slsVersion = 0.111; //Note: commits in develop branch are not numbered here.

	/**
	* Called when an error occurs, for your convenience...
	*/
	err.eventManager = new EventManager({context: err});
	function err(message) {
		err.eventManager.fire({arg: message});
		return Error(message);
	};

	/**
	* @param {Object=} params OPTIONAL: {schemaVersion, columns, linkStyle, groupStyle}
	* @constructor
	*/
	ns.SummaryLinkStore = function SummaryLinkStore(params) {
		//Make sure this is called as a constructor:
		var instance = this;
		if (!(instance instanceof ns.SummaryLinkStore)) {
			throw err("SummaryLinkStore not called as a contructor.");
		}
		
		//The privateKey object is only known to the instance of the SummaryLinkStore (unless you cheat during debugging of course).
		// The value is used to check if an event was triggered by my code, or manually by consuming code.
		//This also allows third parties to write event handlers that (partly) can only be triggered either by internal or consuming code.
		var privateKey = {};
		var viewEventManager = new EventManager({key: privateKey, context: instance});
		var deserializeUrlEventManager = new EventManager({key: privateKey, context: instance});
		
		//This function will be called when one of the view properties is accessed. 
		//Although it does overwrite the default getter/setter behaviour, it does implement exactly the same, accept for firing an event.
		var setViewValue = function viewValueChanged(value) {
			if (arguments.length > 0) {
				var oldValue = this.value;
				this.value = value;
				instance.fireViewListeners.call(privateKey, { 
					propertyName: this.properties.name, 
					oldValue: oldValue 
				});
			}
			return this.value;
		};
		
		//Constructor can be called either with params object or
		// with a serialized string value:
		if(typeof params === "string")
		{
			var toDeserialize = params;
			params = {};
		}
		
		params = params || {}; //params object is optional...
		
		//Initialize the properties of this instance, using values provided in params, when available
		var properties = {
			schemaVersion: {
				type: "number",
				value: params.schemaVersion || 3
			},
			columns: {
				type: "number",
				value: params.columns || 1,
				properties: { name: "columns" },
				accessor: setViewValue
			},
			linkStyle: {
				type: "string",
				value: params.linkStyle || '',
				properties: { name: "linkStyle" },
				accessor: setViewValue
			},
			groupStyle: {
				type: "string",
				value: params.groupStyle || '',
				properties: { name: "groupStyle" },
				accessor: setViewValue
			},
			summaryLinks: new SummaryLinkCollection({owner: instance}) //Note: a specialized accessor is generated from the SummaryLinkCollection instance by design.
		};
		generateAccessors({ instance: instance, properties: properties });

		/**
		* To register a function to listen to changes on the properties: columns, linkStyle and groupStyle.
		* The functions can manually be triggered using fireViewListeners.
		* @param {Object} params REQUIRED: {listener (function)}
		*/
		instance.addViewListener = function addViewListener(params) {
			viewEventManager.add(params);
		};
		
		/**
		* Use this function if a listener should not be fired anymore to changes on the following properties: columns, linkStyle and groupStyle.
		*  Even fireViewListeners would not fire this listener anymore when removed using forgetViewListener.
		* @param {Object} params REQUIRED: {listener (function)}
		*/
		instance.forgetViewListener = function forgetViewListener(params) {
			viewEventManager.forget(params);
		};
		
		/**
		* To register a function that will listen to the following SummaryLink properties to be deserialized: linkUrl, imageUrl
		*  The listener will have a change to manipulate the value and have processingInformation to inform the listener about the context of this value.
		* @param {Object} params REQUIRED: {listener (function)}
		*/
		instance.addDeserializeUrlListener = function addDeserializeUrlListener(params) {
			deserializeUrlEventManager.add(params);
		};
		
		/**
		* Use this function if a listener should not be fired anymore when a SummaryLink property linkUrl or imageUrl is being deserialized.
		* @param {Object} params REQUIRED: {listener (function)}
		*/
		instance.forgetDeserializeUrlListener = function forgetDeserializeUrlListener(params) {
			deserializeUrlEventManager.forget(params);
		};
		
		/**
		* Fire registered by addViewListener.
		* @param {Object=} params OPTIONAL: {propertyName}
		*/
		instance.fireViewListeners = function fireViewListeners(params) {
			viewEventManager.fire({key:this, params: params });
		};
		
		/**
		* Allow custom builder to for instance build a representation of the data.
		* params.builder should be a function called as such:
		  params.builder({
			current: // current item being build (can by SummaryLinkStore or SummaryLinkStore.SummaryLink 
			phase: //"leaf": if can not contain children, "open": before building children, "close": after building children
		  });
		* @param {Object} params REQUIRED: {builder (function)} OPTIONAL {context (used in builder)}
		*/
		instance.build = function build(params) {
			if(!(params && typeof params.builder === "function"))
			{
				throw err("SummaryLinkStore.build called with insufficient arguments.");
			}
			params.builder({current: instance, phase: "open"});
			instance.summaryLinks.build(params);
			params.builder({current: instance, phase: "close"});
		};

		/**
		* Create instances and initialize property values from the stringvalue used in a web part definition
		* @param {!Object} params value: the value to deserialize into a SummaryLinkStore.
		* @return {number} SchemaVersion
		*/
		instance.deserialize = function deserialize(params) {
			if (!(params && typeof params.value === "string")) {
				throw err("SummaryLinkStore.deserialize called with insufficient arguments.");
			}
			
			if(!!params.replace)
			{
				instance.summaryLinks.clear();
			}
			
			var schemaVersion = 1;
			try {
				var serializedValue = makeElement({name: "form", html: params.value});
				var components = findAll({element: serializedValue, name: "div", exclusive: true});

				var postProcessors = [];
				var groupHash = [];

				for (var componentIndex = 0, componentsLength = components.length; componentIndex < componentsLength ; componentIndex++) {
					var element = components[componentIndex];
					var componentTitle = getAttributeValue({element: element, name: "title"});
					switch (componentTitle) {
						case "_schemaversion":
							var _schemaVersion = getAttributeValue({element: element, name: "id"});
							if (typeof _schemaVersion === "string" && _schemaVersion[0] === '_' && _schemaVersion.length > 1) {
								_schemaVersion = +_schemaVersion.substring(1);
								if (!window.isNaN(_schemaVersion)) {
									schemaVersion = _schemaVersion;
								}
								else {
									err("SchemaVersion not recognized: " + _schemaVersion);
								}
							}
							else
							{
								err("SchemaVersion not recognized: " + (_schemaVersion || "(not set)"));
							}
							break;
						case "_links":
							var processed = element;
							//Iterate over div elements under the _links div:
							for(var child = findChildByName({
								element: element, 
								name: "div"
							}); child; child = nextSibblingByName({
								element: child, 
								name: "div"
							}))
							{
								//Div in _links div should be a _link div (representing a SummaryLink instance):
								var childTitlePropertyValue = getAttributeValue({element: child, name: "title"}); 
								if (childTitlePropertyValue === "_link")
								{
									var linkCreationParams = { linkStore: instance, eventKey: privateKey };
									var linkOrder;
									var linkGroup;
									var parent = instance;
									//Iterate over span children, to read the properties of the current SummaryLink
									for (var span = breadthFirst({
										element: child,
										name: "span"
									}); span; span = nextSibblingByName({
										element: span, 
										name: "span"
									}))
									{
										//Check which property is represented by the current span, and map it to linkCreationParams, used to initialize the instance of SummaryLink.
										var linkPropertyTitle = getAttributeValue({element: span, name: "title"});
										switch (linkPropertyTitle) { 
											case "_title":
												linkCreationParams.title = getNodeText({element: span});
												break;
											case "_linkurl": 
												var urlInfo = { 
													schemaVersion: schemaVersion,
													propertyName: "linkUrl", 
													direction: "deserialize",
													value: "", 
													processingInfo: {
														span: span
													}
												};
												
												var anchor = depthFirst({element: span, name:"a"});
												if(anchor)
												{
													var href = getAttributeValue({element: anchor, name: "href"});
													var anchorTextValue = getNodeText({element: anchor});
													urlInfo.processingInfo.innerHTML = anchor.innerHTML;
													urlInfo.processingInfo.href = href;
													urlInfo.processingInfo.nodeText = anchorTextValue;
													urlInfo.value = href || anchorTextValue;
												}
												
												deserializeUrlEventManager.fire({key: privateKey, params: urlInfo});
												
												linkCreationParams.linkUrl = urlInfo.value;
												break;
											case "_order":
												linkOrder = +getNodeText({element: span}); //Order can only (and will) be set after all SummaryLinks are imported.
												break;
											case "_group":
												//Groups are unfortunately referenced by title (which is not required to be unique).
												// If the definition of a referred group title has already been deserialized, use the latest group by that title, 
												//  assuming definition is ordered that way.
												//If currently no group by this title has been deserialized, wait until every SummaryLink instance has been deserialized
												// then attempt if a group by the title is known and otherwise create the missing group.
												linkGroup = getNodeText({element: span});
												if (groupHash[linkGroup]) //In this case the group has been already deserialized and is interpreted as the referred group.
												{
													parent = linkGroup = groupHash[linkGroup];
													linkCreationParams.group = linkGroup.id ? linkGroup.id() : getNodeText({element: span});
												}
												break;
											case "_style":
												linkCreationParams.style = getNodeText({element: span});
												break;
											case "_begincolumn":
												linkCreationParams.beginColumn = getNodeText({element: span}).toLowerCase() === "true";
												break;
											case "_isgroupheader":
												linkCreationParams.isGroupHeader = getNodeText({element: span}).toLowerCase() === "true";
												break;
											case "_description":
												linkCreationParams.description = getNodeText({element: span});
												break;
											case "_imageurl":
												var urlInfo = { 
													schemaVersion: schemaVersion,
													propertyName: "imageUrl", 
													direction: "deserialize",
													value: "", 
													processingInfo: {
														span: span
													}
												};
												
												switch(schemaVersion)
												{
													case 1:
														var imgInfoElement = depthFirst({element: span, name:"img"});
														if(imgInfoElement)
														{
															urlInfo.processingInfo.innerHTML = imgInfoElement.innerHTML;
															urlInfo.value = getAttributeValue({element: imgInfoElement, name: "src"});
														}
														break;
													case 2:
													case 3:
														var anchorInfoElement = depthFirst({element: span, name:"a"});
														if(anchorInfoElement)
														{
															urlInfo.processingInfo.innerHTML = anchorInfoElement.innerHTML;
															urlInfo.value = getAttributeValue({
																element: anchorInfoElement, 
																name: "href"
															});
														}
														break;
													default:
														err("Unsupported schema version: " + schemaVersion);
												}
												
												deserializeUrlEventManager.fire({key: privateKey, params: urlInfo});
												
												linkCreationParams.imageUrl = urlInfo.value;
												
												break;
											case "_imageurlalttext":
												linkCreationParams.imageUrlAltText = getNodeText({element: span});
												break;
											case "_linktooltip":
												linkCreationParams.linkToolTip = getNodeText({element: span});
												break;
											case "_openinnewwindow":
												linkCreationParams.openInNewWindow = getNodeText({element: span}).toLowerCase() === "true";
												break;
											case "_groupstyle":
												linkCreationParams.groupStyle = getNodeText({element: span});
												break;
											case "_loginname":
												linkCreationParams.loginName = getNodeText({element: span});
												break;
											case "_sipaddress":
												linkCreationParams.sipAddress = getNodeText({element: span});
												break;
											default:
												err("Unknown property ignored: " + linkPropertyTitle);
										}
									}

									//Create instance from read properties, then add the SummaryLink to the SummaryLinkStore
									var item = new ns.SummaryLinkStore.SummaryLink(linkCreationParams);
									parent.summaryLinks.add({ item: item });

									//If current item is a group, then the instance should be cached by title (or overwrite previous cache)
									// to interpret referency by title in following links.
									if (item.isGroupHeader())
									{
										groupHash[linkCreationParams.title] = item;
									}

									if(typeof linkOrder === "number" || typeof linkGroup === "string")
									{
										var delegate = createDelegate({
											item: item,
											order: linkOrder,
											group: linkGroup
										}, function processor() {
											var item = this.item;
											var group = this.group;
											var order = this.order;
											if (typeof group === "string") {
												var container = groupHash[group];
												if (typeof container === "object" && container instanceof ns.SummaryLinkStore.SummaryLink) {
													container.summaryLinks.add({ item: item });
													group = container;
												}      
												else {
													var hasMatch = false;
													var foundItems = instance.summaryLinks({ title: group });
													for(var i = foundItems.length-1; i>=0; i--)
													{
														var current = foundItems[i];
														if (current && current.isGroupHeader && current.isGroupHeader())
														{
															hasMatch = true;
															current.summaryLinks.add({ item: item });
															break;
														}
													}
													if (!hasMatch) {
														var groupHeader = new ns.SummaryLinkStore.SummaryLink({ isGroupHeader: true, title: group, eventKey: privateKey });
														instance.summaryLinks.add({ item: groupHeader });
														groupHeader.summaryLinks.add({ item: item });
													}
												}
											}
											if (typeof order === "number") {
												item.order(order);
											}
										});
										if (typeof linkOrder === "number")
										{
											delegate.order = linkOrder; //Delegates will be sorted by this value.
										}
										postProcessors.push(delegate);
									}
								}
								else
								{
									err("Expected _link, but found: " + childTitlePropertyValue);
								}
								processed = child;
							}
							componentIndex = getIndex({collection: components, instance: processed});
							break;
						case "_groups":
							for (var span = breadthFirst({
								element: element,
								name: "span"
							}); span; span = nextSibblingByName({
								element: span, 
								name: "span"
							})) {
								var groupPropertyTitle = getAttributeValue({element: span, name: "title"}); 
								if (groupPropertyTitle === "_title") {
									var title = getNodeText({element: span});
									var groupHeader = new ns.SummaryLinkStore.SummaryLink({ isGroupHeader: true, title: title, eventKey: privateKey });
									instance.summaryLinks.add({ item: groupHeader });
									groupHash[title] = groupHeader;
								}
								else
								{
									err("expected _title, but found (and ignored): " + groupPropertyTitle);
								}
							}
							break;
						case "_view":
							for (var span = breadthFirst({
								element: element,
								name: "span"
							}); span; span = nextSibblingByName({
								element: span, 
								name: "span"
							})) {
								var viewPropertyTitle = getAttributeValue({element: span, name: "title"});
								switch (viewPropertyTitle) {
									case "_columns":
										instance.columns(+getNodeText({element: span}));
										break;
									case "_linkstyle":
										instance.linkStyle(getNodeText({element: span}));
										break;
									case "_groupstyle":
										instance.groupStyle(getNodeText({element: span}));
										break;
									default:
										err("Unknown property ignored: " + viewPropertyTitle);
								}
							}
							break;
					}
				}

				postProcessors.sort(function sortProcessors(a, b) {
					if (a.order === -1 && b.order !== -1)
					{
						return 1;
					}
					return a.order < b.order ? -1 : (a.order > b.order ? 1 : 0);
				});

				for(var i = 0, n = postProcessors.length; i<n; i++)
				{
					postProcessors[i]();
				}
			}
			catch (e) {
				err("Summary link XML value corrupted (or there is a bug in the code, but that never happens).");
			}

			instance.schemaVersion(schemaVersion);
			return schemaVersion;
		};

		/**
		* @param {Object=} params Optionally provide a context to serialize.
		* @return {string} SummaryLinkFieldValue as string.
		*/
		instance.serialize = function serialize(params) {
			params = params || {};
			var context = params.context = params.context || {};
			context.order = context.order || 0;
			context.groupNr = context.groupNr || 0;
			var strings = context.strings = context.strings || [];
			
			//Root (with schemaversion attribute)
			strings.push('<div title="_schemaversion" id="_' + instance.schemaVersion() + '">');
			{
				//Serialize content (SummaryLinks):
				instance.summaryLinks.serialize({ context: context, deserializeUrlEventManager: deserializeUrlEventManager });

				//Serialize view:
				strings.push('<div title="_view">');
				{
					strings.push('<span title="_columns">' + instance.columns() + '</span>');
					strings.push('<span title="_linkstyle">' + instance.linkStyle() + '</span>');
					strings.push('<span title="_groupstyle">' + instance.groupStyle() + '</span>');
				}
				strings.push('</div>');
			}
			strings.push('</div>');

			return strings.join('');
		};

		/**
		* Create string value to be used in web part definition
		* @param {Object=} params Optionally provide a context to serialize.
		* @return {string} SummaryLinkFieldValue escaped to be included in a webpart property.
		*/
		instance.toString = function toString(params) {
			return escapeTextForXMLInsertion({ text: instance.serialize.apply(instance, arguments) });
		};
		
		//If constructor was called with a string value, deserialize that value as the content.
		if(typeof toDeserialize === "string")
		{
			instance.deserialize({
				value: toDeserialize
			});
		}
		
		/**
		* If you wish to get an API may be easier, but is not supported by older browsers (like IE8):
		*/
		var $$reflective;
		instance.$$reflective = function get$$reflective() {
			if(!$$reflective)
			{
				$$reflective = generate$$reflective({ instance: instance, properties: properties });
				$$reflective.toString = createDelegate(instance, instance.toString);
			}
			return $$reflective;
		};
	};

	/**
	* If you wish to listen to errors, just call this function with a listener that takes the message string as argument.
	* Using additional try/catch patterns is recommended.
	*/
	ns.SummaryLinkStore.addErrorListener = function addErrorListener(listener) {
		err.eventManager.add(listener);
	};

	/**
	* Since SummaryLinkCollection is a private constructor, this function helps to typecheck an instance externally.
	* Only during build, the instance of SummaryLinkStore becomes available to the builder function.
	*/
	ns.SummaryLinkStore.isSummaryLinkCollection = function isSummaryLinkCollection(instance)
	{
		return instance instanceof SummaryLinkCollectionWrapper;
	};

	/**
	* @constructor
	*/
	function SummaryLinkCollectionWrapper(summaryLinkCollection)
	{
		var instance = this;
		if(!((instance instanceof SummaryLinkCollectionWrapper)&&(summaryLinkCollection instanceof SummaryLinkCollection)))
		{
			throw err("internal error");
		}
		instance.owner = createDelegate(summaryLinkCollection, summaryLinkCollection.owner);
		instance.canParent = createDelegate(summaryLinkCollection, summaryLinkCollection.canParent);
	}

	/**
	* The instance created by this constructor, can be used as a property of an other object.
	*  This property should then be initialized with generateAccessors to be functional.
	* @param {Object=} params OPTIONAL: {owner (may be set after creation, but should be set)}
	* @constructor
	*/
	function SummaryLinkCollection(params) {
		params = params || {};
				
		var instance = this;
		if (!(instance instanceof SummaryLinkCollection)) {
			throw err("Not called as a constructor.");
		}

		//Properties to which accessors are generated for SummaryLink instances:
		var properties = {
			owner: {
				type: "any",
				accessor: function ownerAccessor() {
					if(arguments.length > 0)
					{
						this.value = getInstance(arguments[0]);
					}
					return this.value;
				},
				validator: function validateOwner(value) {
					return (getInstance(value) instanceof ns.SummaryLinkStore) || (getInstance(value) instanceof ns.SummaryLinkStore.SummaryLink);
				},
				value: params.owner
			},
			canParent: {
				type: "ReadOnly",
				getter: function getCanParent() {
					var owner = instance.owner();
					return owner instanceof ns.SummaryLinkStore || (owner.isGroupHeader && owner.isGroupHeader());
				}
			}
		};
		generateAccessors({ instance: instance, properties: properties });
		var wrapper = new SummaryLinkCollectionWrapper(instance);
		
		var items$$modified = true;
		var $$reflectiveItems = [];
		//When using $$reflective api, this will be the getter for the SummaryLinks api...
		instance.$$reflective = function get$$reflective() {
			var result = {instance: wrapper};
			for(var key in instance.properties)
			{
				result[key] = createDelegate({key:key}, function handler(){
					return instance.properties[this.key].apply(instance.$$reflective.reference, arguments);
				});
			}
			Object.defineProperty(result, "items", {
				configurable: false,
				enumerable: true,
				get: function getItems() {
					if(items$$modified)
					{
						$$reflectiveItems.length = 0;
						var items = instance.accessor();
						for(var i = 0, n = items.length; i<n; i++)
						{
							var item = items[i];
							var modernItem = item.$$reflective ? item.$$reflective() : item;
							$$reflectiveItems.push(modernItem);
						}
						items$$modified = false;
					}
					var copy = $$reflectiveItems.slice(0);
					copy.valueOf=function valueOfCopy(){
						return 0; //allow .summaryLinks.items++ to create a new item...
					};
					return copy;
				},
				set: function setItems(value) {
					if(typeof value === "number")
					{
						var item = $$ReflectiveReferences[Math.abs(value)-2];
						if(value > 1)
						{
							instance.$$reflective.reference.add({item:item});
						}
						if(value <- 1)
						{
							instance.$$reflective.reference.remove({item:item});
						}
						if(value === 1)
						{
							err("WARNING: eegg");
							var items = instance.accessor();
							var hasGroups = !!(items && items.length && items[0] && items[0].isGroupHeader && items[0].isGroupHeader());
							instance.$$reflective.reference.add({item: new ns.SummaryLinkStore.SummaryLink(hasGroups ? {
								isGroupHeader: true, title: "Group"} : {
								title: "Documentation on how to use SummaryLinkStore.js",
								linkUrl: "http://summarylinkstore.codeplex.com/"
							})});
						}
					}
					else
					{
						throw err("Not implemented.");
					}
				}
			});
			return result;
		};
		
		var items = [];
		var hash = {};
		instance.type = "any";
		instance.value = { items: items, hash: hash, groupHeaderCount: 0, groupHash: {} };
		instance.accessor = function getSummaryLink() {
			if (arguments.length > 0 && typeof arguments[0] === "object" && arguments[0] !== null)
			{
				/**
				* In this case the accessor is called with arguments to query for a specific item.
				*/
				var params = arguments[0];
				var result = [];
				if (params.id && hash[params.id]) {
					result.push(hash[params.id]);
				}
				if (params.title) {
					for (var i = 0, n = items.length; i < n; i++) {
						var item = items[i];
						if (item.title() === params.title) {
							result.push(item);
						}
					}
				}
				return result;
			}
			else
			{
				return items.slice(0); //Return a copy of the items array.
			}
		};
		instance.properties = {
			add: function addItem(params) {
				if (!(params && (getInstance(params.item) instanceof ns.SummaryLinkStore.SummaryLink))) {
					throw err("SummaryLinkCollection.add called with insufficient arguments.");
				}
				if (!instance.canParent()) {
					throw err("SummaryLinkCollection can not get any children (SummaryLinkCollection.add).");
				}
				var collection = this;
				var item = getInstance(params.item);

				//Register current linkstore to item.
				var owner = instance.owner();
				var originalLinkStore = item.linkStore();
				if (owner && ((owner instanceof ns.SummaryLinkStore) || (owner.linkStore && (owner.linkStore() instanceof ns.SummaryLinkStore)))) {
					item.linkStore((owner instanceof ns.SummaryLinkStore) ? owner : owner.linkStore());
				}

				if (item.isGroupHeader()) {
					var first = items[0];
					instance.value.groupHeaderCount++;
					instance.value.groupHash[item.id()] = item;
					items.push(item);
					hash[item.id()] = item;
					items$$modified = true;

					//If the linkstore contains groups, all items should be in a group.
					// So when a group is added while not all items are in a group: add those items to the new group.
					if (first && (first instanceof ns.SummaryLinkStore.SummaryLink) && !first.isGroupHeader()) {
						var originalItems = collection(); //get copy of items, to be able to both iterate and modify the collection:
						for (var i = 0, n = originalItems.length; i < n; i++) {
							var current = originalItems[i];
							if (current && (current instanceof ns.SummaryLinkStore.SummaryLink) && !current.isGroupHeader()) {
								item.summaryLinks.add({ item: current });
							}
						}
					}
				}
				else {
					//Item can be added from a different owner.
					if (item.linkStore()) {
						var result = (originalLinkStore || item.linkStore()).summaryLinks.find({ id: item.id() });
						if (result.found) {
							result.owner.summaryLinks.remove({ item: item });
						}
					}

					var first = items[0];
					if (first && (first instanceof ns.SummaryLinkStore.SummaryLink) && first.isGroupHeader()) {
						first.summaryLinks.add(params);
					}
					else {
						items.push(item);
						hash[item.id()] = item;
						items$$modified = true;
						
						if (owner instanceof ns.SummaryLinkStore) {
							item.group.clear();
						}
						else {
							item.group(owner.id());
						}
					}
				}
			},
			addEach: function addEach(params) {
				if (!(params && Object.prototype.toString.apply(params.items) === '[object Array]')) {
					throw err("SummaryLinkCollection.addEach called with insufficient arguments.");
				}
				var hasErrors = false;
				for (var i = 0, n = params.items.length; i < n; i++) {
					try
					{
						this.add({ item: params.items[i] });
					}
					catch(e)
					{
						err(e&&e.message||e);
						hasErrors = true;
					}
				}
				if(hasErrors)
				{
					throw err("SummaryLinkCollection.addEach could not add each of the provided items.");
				}
			},
			build: function build(params) {
				if (!(params && typeof params.builder === "function")) {
					throw err("SummaryLinkCollection.build called with insufficient arguments.");
				}
				params.builder({ current: wrapper, phase: "open" });
				for (var i = 0, n = items.length; i < n; i++) {
					items[i].build(params);
				}
				params.builder({ current: wrapper, phase: "close" });
			},
			clear: function clearItems() {
				items.length = 0;
				items$$modified = true;
				
				instance.value.groupHeaderCount = 0;
				instance.value.groupHash = {};
				for (var key in hash) {
					if (hash[key] instanceof ns.SummaryLinkStore.SummaryLink && hash[key].isGroupHeader()) {
						hash[key].summaryLinks.clear();
					}
					delete hash[key];
				}
			},
			count: function countItems(params) {
				params = params || {};
				var count = items.length;
				var groups = [];
				var groupsLength = 0;
				if (params.recursive || params.linksOnly) {
					groups = this.groups();
					groupsLength = groups.length;
				}
				if (params.recursive) {
					for (var i = 0, n = groups.length; i < n; i++) {
						count += groups[i].summaryLinks.count(params);
					}
				}
				if (params.linksOnly)
				{
					count -= groupsLength;
				}
				return count;
			},
			find: function findItem(params) {
				if (!(params && params.id)) {
					throw err("SummaryLinkCollection.find called with insufficient arguments.");
				}
				var id = params.id;
				if (hash[id]) {
					return {
						found: true,
						item: hash[id],
						owner: instance.owner()
					};
				}
				else {
					var groups = this.groups();
					for (var i = 0, n = groups.length; i < n; i++) {
						var group = groups[i];
						var result = group.summaryLinks.find({ id: id });
						if (result.found) {
							return result;
						}
					}
				}
				return { found: false };
			},
			groupHeaderCount: function getGroupHeaderCount() {
				return instance.value.groupHeaderCount;
			},
			groups: function getGroups() {
				var groups = [];
				for (var i = 0, n = items.length; i < n; i++) {
					var current = items[i];
					if (instance.value.groupHash[current.id()]) {
						groups.push(current);
					}
				}
				return groups;
			},
			newGroupName: function newGroupName() {
				var collection = this;
				var groupName = "Group " + collection.groupHeaderCount();
				if(collection({title:groupName}).length > 0)
				{
					groupName += " " + newGuid();
				}
				return groupName;
			},
			order: function manageItemorder(params) {
				if (!(params && (getInstance(params.item) instanceof ns.SummaryLinkStore.SummaryLink) || (params.id))) {
					throw err("SummaryLinkCollection.order called with insufficient arguments.");
				}
				var id = params.id || getInstance(params.item).id();
				var newOrder = params.newOrder;
				var doMove = typeof newOrder === "number";
				var newIndex = -1;
				var result = { found: false, order: params.order || 0, owner: null };

				var owned = hash[id];

				var index = 0;
				for (var count = items.length; index < count; index++) {
					result.order++;
					var item = items[index];
					if (doMove && (!result.moved) && newIndex === -1 && result.order >= newOrder) {
						newIndex = index;
						if (result.found) {
							break;
						}
					}

					if (result.found) {
						if (item.isGroupHeader && item.isGroupHeader()) {
							result.order += item.summaryLinks.count();
						}
					}
					else {
						if (item && item.id && item.id() === id) {
							result.found = true;
							result.owner = instance.owner();
							result.ownerIndex = index;
							if (newIndex >= 0 || !doMove) {
								break;
							}
						}
						if (item.isGroupHeader && item.isGroupHeader()) {
							if (owned) {
								result.order += item.summaryLinks.count();
							}
							else {
								var subResult = item.summaryLinks.order({ id: id, order: result.order, newOrder: newOrder });
								result.order = subResult.order;
								if (subResult.found) {
									result.moved = result.moved || subResult.moved;
									result.found = true;
									result.owner = item;
									result.ownerIndex = subResult.ownerIndex;
									break;
								}
							}
						}
					}
				}

				if (doMove && newIndex >= 0 && result.owner === instance.owner()) {
					var step = 0;
					var currentIndex = result.ownerIndex;
					try
					{
						step = (newIndex - currentIndex) / Math.abs(newIndex - currentIndex); //-1 or 1, depending on direction. or NaN when dividing by zero (hence try/catch)
					}
					catch(e)
					{
						err(e&&e.message||e);
					}
					if(step != 0 && !window.isNaN(step))
					{
						for (var length = items.length; currentIndex != newIndex; currentIndex += step) {
							var itemToMove = items[currentIndex];
							items[currentIndex] = items[currentIndex + step];
							items[currentIndex + step] = itemToMove;
							items$$modified = true;
						}
					}
				}

				return result;
			},
			move: function moveItem(params) {
				/**
				* Move item up or down one step. 
				*  params.item or params.id defines which item to move
				*  params.direction defines if the item should move "up" or "down"
				* returns -1: if item was found, but could not be moved in provided direction; -2: if item was not found; number >= 0: new index of item after moving.
				*/
				if (!(params && params.direction in { down: -1, up: 1 } && (getInstance(params.item) instanceof ns.SummaryLinkStore.SummaryLink) || params.id)) {
					throw err("SummaryLinkCollection.move called with insufficient arguments.");
				}
				var item = getInstance(params.item);
				var direction = params.direction;
				var unbound = !!params.unbound;
				if (!(item instanceof ns.SummaryLinkStore.SummaryLink)) {
					item = hash[params.id];
				}
				var collection = this;
				
				if (!(item instanceof ns.SummaryLinkStore.SummaryLink)) {
					var groups = collection.groups();
					for (var i = 0, n = groups.length; i < n; i++) {
						var previous = group;
						var group = groups[i];
						if (group.summaryLinks) {
							var result = group.summaryLinks.move(params);
							if (result === -1 && unbound)
							{
								var matchingItem = group.summaryLinks(params)[0];
								if(matchingItem instanceof ns.SummaryLinkStore.SummaryLink)
								{
									if(direction === "up" && previous instanceof ns.SummaryLinkStore.SummaryLink)
									{
										previous.summaryLinks.add({item: matchingItem});
										return -3;
									}
									if(direction === "down")
									{
										var nextGroup = groups[i+1];
										if(nextGroup instanceof ns.SummaryLinkStore.SummaryLink)
										{
											nextGroup.summaryLinks.add({item: matchingItem});
											matchingItem.order(0);
											return -3;
										}
									}
								}
							}
							if (result > -2) {
								return result;
							}
						}
					}
				}
				else {
					var previous = items[0];
					if (direction === "up" && previous === item) {
						return -1; //If first in collection: can not move up further.
					}
					for (var i = 1, n = items.length; i < n; i++) {
						var current = items[i];
						if ((direction === "up" && item === current) || (direction === "down" && item === previous)) {
							items[i] = previous;
							items[i - 1] = current;
							items$$modified = true;
							return i - 1;
						}
						previous = current;
					}
					if (direction === "down" && previous === item) {
						return -1; //If last in collection: can not move down further.
					}
				}
				return -2;
			},
			moveDown: function moveItemDown(params) {
				if (!(params && (getInstance(params.item) instanceof ns.SummaryLinkStore.SummaryLink) || (params.id && params.unbound || hash[params.id]))) {
					throw err("SummaryLinkCollection.moveDown called with insufficient arguments.");
				}
				params.direction = "down";
				return this.move(params);
			},
			moveUp: function moveItemUp(params) {
				if (!(params && (getInstance(params.item) instanceof ns.SummaryLinkStore.SummaryLink) || (params.id && params.unbound || hash[params.id]))) {
					throw err("SummaryLinkCollection.moveUp called with insufficient arguments.");
				}
				params.direction = "up";
				return this.move(params);
			},
			remove: function removeItem(params) {
				if (!(params && (getInstance(params.item) instanceof ns.SummaryLinkStore.SummaryLink) || (params.id))) {
					throw err("SummaryLinkCollection.remove called with insufficient arguments.");
				}
				var item = getInstance(params.item);
				if (!(item instanceof ns.SummaryLinkStore.SummaryLink)) {
					item = hash[params.id];
				}
				if (!(item instanceof ns.SummaryLinkStore.SummaryLink)) {
					var groups = this.groups();
					for (var i = 0, n = groups.length; i < n; i++) {
						var group = groups[i];
						if (group.summaryLinks) {
							var groupResult = group.summaryLinks.remove(params);
							if (groupResult.length > 0) {
								return groupResult;
							}
						}
					}
				}
				else {
					var indices = [];
					for (var i = items.length - 1; i >= 0; i--) {
						var current = items[i];
						if (current === item) {
							if (item.isGroupHeader()) {
								delete instance.value.groupHash[item.id()];
								instance.value.groupHeaderCount--;
							}
							items.splice(i, 1);
							delete hash[item.id()];
							indices.push(i);
							items$$modified = true;
						}
					}
					return indices;
				}
				return [];
			},
			serialize: function serializeItems(params) {
				params = params || {};
				var context = params.context || {};
				var deserializeUrlEventManager = params.deserializeUrlEventManager || new EventManager({key:{}, context:instance});
				context.order = context.order || 0;
				context.groupNr = context.groupNr || 0;
				var strings = context.strings = context.strings || [];

				//SummaryLinks:
				strings.push('<div title="_links">');
				{
					for (var i = 0, n = items.length; i < n; i++) {
						var item = items[i];
						item.serialize({ context: context, deserializeUrlEventManager: deserializeUrlEventManager });
					}
				}
				strings.push('</div>');
				return context;
			},
			sort: function sortItems(params) {
				params = params || {};
				var direction = params.descending ? -1 : 1;
				var property = params.property || "title";
				if(typeof params.sortcode == "string")
				{
					var codes = params.sortcode.split('_');
					if(codes.length > 1)
					{
						direction = codes[1][0] === 'a' ? 1 : -1;
					}
				}

				items.sort(typeof params === "function" ? params : function sortItems(item1, item2) {
					return direction * item1[property]().localeCompare(item2[property]());
				});
				items$$modified = true;
			},
			ungroup: function ungroupItem(params) {
				if (!(params && params.id || (getInstance(params.item) instanceof ns.SummaryLinkStore.SummaryLink))) {
					throw err("SummaryLinkCollection.ungroup called with insufficient arguments");
				}
				var collection = this;
				var id = (getInstance(params.item) instanceof ns.SummaryLinkStore.SummaryLink) ? getInstance(params.item).id() : params.id;
				var group = instance.value.groupHash[id];
				if(!group)
				{
					throw err("Could not find group in collection.");
				}
				var items = group.summaryLinks();
				collection.remove({ item: group });
				collection.addEach({ items: items });
			},
			updateEach: function updateEach(params) {
				if(!(params && typeof params.property === "string" && "value" in params))
				{
					throw err("SummaryLinkCollection.updateEach called within sufficient arguments");
				}
				var property = params.property;
				var value = params.value;
				var all = items.slice(0);
				for (var i = 0, n = all.length; i < n; i++)
				{
					var current = all[i];
					current[property](value);
					current.summaryLinks.updateEach(params);
				}
			}
		};
	};
	/**
	 * @param {Object=} params OPTIONAL initial property values, where an initial value is supported.
	 * @constructor
	 */
	ns.SummaryLinkStore.SummaryLink = function SummaryLinkStore(params) {
		var instance = this;
		if (!(instance instanceof ns.SummaryLinkStore.SummaryLink)) {
			throw err("oPB.SummaryLinkStore.SummaryLink not called as a constructor.");
		}
		
		var privateEventKey = params.eventKey || {};
		var initialLinkUrl = params.linkUrl;
		var initialImageUrl = params.imageUrl;
		
		//Properties to which accessors are generated for SummaryLink instances:
		var properties = {
			beginColumn: {
				type: "boolean",
				value: params.beginColumn //value which was read, not the value which will be written, that is defined in this.isBeginColumn.
			},
			description: {
				type: "string",
				max: 1000,
				value: params.description
			},
			group: {
				type: "string",
				optional: true, //provides clear function to set to undefined.
				properties: {
					isGuid: function isGuid() {
						try {
							return isValidGuid(this());
						} catch (e) {
							return false;
						}
					}
				},
				value: params.group
			},
			groupStyle: {
				type: "string",
				value: params.groupStyle
			},
			imageUrl: {
				type: "string",
				value: params.imageUrl
			},
			imageUrlAltText: {
				type: "string",
				max: 500,
				value: params.imageUrlAltText
			},
			isGroupHeader: {
				type: "ReadOnly",
				value: params.isGroupHeader
			},
			isPeopleLink: {
				type: "ReadOnly",
				getter: function getIsPeopleLink() {
					return !!instance.loginName();
				}
			},
			linkStore: {
				type: "any",
				accessor: function linkStoreAccessor() {
					if(arguments.length > 0)
					{
						this.value = getInstance(arguments[0]);
					}
					return this.value;
				},
				validator: function checkLinkStoreType(value) {
					return getInstance(value) instanceof ns.SummaryLinkStore;
				},
				properties: { internal: true },
				value: getInstance(params.linkStore) instanceof ns.SummaryLinkStore ? getInstance(params.linkStore) : undefined
			},
			linkToolTip: {
				type: "string",
				max: 500,
				value: params.linkToolTip
			},
			linkUrl: {
				type: "string",
				value: params.linkUrl
			},
			loginName: {
				type: "string",
				value: params.loginName
			},
			openInNewWindow: {
				type: "boolean",
				value: params.openInNewWindow
			},
			order: {
				type: "any",
				accessor: function orderAccessor() {
					var params = { id: instance.id() };
					var parent = instance.parent();
					if (arguments.length > 0) {
						var newOrder = +arguments[0];
						if (!window.isNaN(newOrder)) {
							if ((!(parent instanceof ns.SummaryLinkStore)) && parent.id) {
								var bias = instance.linkStore().summaryLinks.order({ id: parent.id() }).order;
								if (!window.isNaN(+bias)) {
									newOrder -= bias;
								}
							}
							params.newOrder = newOrder;
						}
					}
					return parent && parent.summaryLinks.order(params);
				}
			},
			parent: {
				type: "readOnly",
				getter: function getParent() {
					var result;
					var linkStore = instance.linkStore();
					if (linkStore) {
						var findResult = linkStore.summaryLinks.find({ id: instance.id() });
						if (findResult.found) {
							result = findResult.owner;
						}
					}
					return result;
				}
			},
			sipAddress: {
				type: "string",
				value: params.sipAddress
			},
			style: {
				type: "string",
				optional: true,
				value: params.style
			},
			summaryLinks: new SummaryLinkCollection({ owner: instance }),
			title: {
				type: "string",
				max: 500,
				value: params.title
			},
			id: {
				type: "readOnly",
				getter: function getID() {
					return this.value + "";
				},
				value: newGuid({braces:true})
			}
		};

		generateAccessors({ instance: instance, properties: properties });
		
		/**
		* Calculates if the current summarylink is the begin of a column.
		* @param {Object} params REQUIRED: {context}
		* @return {boolean}
		*/
		instance.isBeginColumn = function isBeginColumn(params) {
			if (!(params || params.context)) {
				throw err("SummaryLink.isBeginColumn called with insufficient arguments.");
			}
			var context = params.context;

			var output = params.output || {}; //If no output is provided, the output object is lost at end of function execution.

			var linkStore = instance.linkStore();
			var groupCount = linkStore.summaryLinks.groupHeaderCount();
			var result = false;
			if (context.order === 1) {
				result = true; //First item is always begin of the first column.
				output.columnNr = 1;
			}
			if (instance.isGroupHeader()) {
				if (context.groupNr === 1) {
					result = true; //First group is always begin of the first column (not checking for order===1 is intentional)
					output.columnNr = 1;
				}
				else {
					var columns = +linkStore.columns();
					if ((!window.isNaN(columns)) && columns > 1) {
						var groupsPerColumn = Math.floor(groupCount / columns);
						var leftoverColumns = (groupCount % columns);
						var position = 1;
						for (var column = 2; column <= columns; column++) {
							position += groupsPerColumn;

							var previousColumn = column - 1;
							if (previousColumn <= leftoverColumns) {
								position++; //Previous column had one more item, so the next column starts one position later.
							}
							if (context.groupNr === position) {
								result = true;
								output.columnNr = column;
							}
						}
					}
				}
			}

			return result;
		};

		/**
		* Allow custom builder to for instance build a representation of the data.
		* params.builder should be a function called as such:
		  params.builder({
			current: // current item being build (can by SummaryLinkStore or SummaryLinkStore.SummaryLink 
			phase: //"leaf": if can not contain children, "open": before building children, "close": after building children
		  });
		*/
		instance.build = function build(params) {
			if (!(params && typeof params.builder === "function")) {
				throw err("SummaryLinkStore.build called with insufficient arguments.");
			}
			if (instance.isGroupHeader()) {
				params.builder({ current: instance, phase: "open" });
				instance.summaryLinks.build(params);
				params.builder({ current: instance, phase: "close" });
			}
			else {
				params.builder({ current: instance, phase: "leaf" });
			}
		};

		instance.serialize = function serialize(params) {
			params = params || {};
			var deserializeUrlEventManager = params.deserializeUrlEventManager || new EventManager({key:privateEventKey, context: instance});
			
			var context = params.context || {};
			context.order = context.order || 0;
			context.groupNr = context.groupNr || 0;
			var strings = context.strings = context.strings || [];
			
			context.order++;
			if (instance.isGroupHeader()) {
				context.groupNr++;
			}

			//For values which are inserted in the xml, and eventually that value is used in html:
			var escapeForHTML = function escapeForInsertion(value) {
				return escapeTextForHTMLInsertion({ text: value });
			};

			strings.push('<div title="_link">');
			{
				strings.push('<span title="_title">' + escapeForHTML(instance.title()) + '</span>');
				strings.push('<span title="_order">' + context.order + '</span>');

				var isBeginColumn = instance.isBeginColumn({ context: context }) ? 'True' : 'False';
				strings.push('<span title="_begincolumn">' + isBeginColumn + '</span>');

				if (instance.isGroupHeader()) {
					strings.push('<span title="_isgroupheader">True</span>');
					strings.push('<span title="_groupstyle">' + instance.linkStore().groupStyle() + '</span>');
				}
				else {
					if (instance.isPeopleLink()) {
						strings.push('<span title="_loginname">' + instance.loginName() + '</span>');
						strings.push('<span title="_sipaddress">' + instance.sipAddress() + '</span>');
					}

					if (typeof instance.description() !== "undefined") {
						strings.push('<span title="_description">' + escapeForHTML(instance.description()) + '</span>');
					}
					if (typeof instance.group() !== "undefined") {
						var value = instance.group();
						if (instance.group.isGuid() && instance.linkStore()) {
							var findResult = instance.linkStore().summaryLinks.find({
								id: value
							});
							if (findResult.found) {
								value = findResult.item.title();
							}
						}
						strings.push('<span title="_group">' + escapeForHTML(value) + '</span>');
					}
					if (typeof instance.linkUrl() !== "undefined") {
						strings.push('<span title="_linkurl">');
						{
							var urlInfo = {
								schemaVersion: 3,
								propertyName: "linkUrl",
								direction: "serialize",
								value: instance.linkUrl(),
								processingInfo: {
									initialValue: initialLinkUrl,
									serialize:true
								}
							};
							deserializeUrlEventManager.fire({ key: privateEventKey, params: urlInfo});
							strings.push('<a href="' + (""+urlInfo.value).replace('"', "&quot;") + '">' + escapeForHTML(urlInfo.value) + '</a>');
						}
						strings.push('</span>');
					}
					if (typeof instance.linkToolTip() !== "undefined") {
						strings.push('<span title="_linktooltip">' + escapeForHTML(instance.linkToolTip()) + '</span>');
					}
					if (typeof instance.imageUrl() !== "undefined") {
						strings.push('<span title="_imageurl">');
						{
							var urlInfo = {
								schemaVersion: 3,
								propertyName: "imageUrl",
								direction: "serialize",
								value: instance.imageUrl(),
								processingInfo: {
									initialValue: initialImageUrl,
									serialize:true
								}
							};
							deserializeUrlEventManager.fire({ key: privateEventKey, params: urlInfo});
							strings.push('<a href="' + (""+urlInfo.value).replace('"', "&quot;") + '">' + escapeForHTML(urlInfo.value) + '</a>');
						}
						strings.push('</span>');
					}
					if (typeof instance.imageUrlAltText() !== "undefined") {
						strings.push('<span title="_imageurlalttext">' + escapeForHTML(instance.imageUrlAltText())+ '</span>');
					}
					if (typeof instance.style() !== "undefined") {
						strings.push('<span title="_style">' + ((instance.style() || instance.linkStore() && instance.linkStore().linkStyle()) || '') + '</span>');
					}
					if (typeof instance.openInNewWindow() !== "undefined") {
						var value = instance.openInNewWindow() ? 'True' : 'False';
						strings.push('<span title="_openinnewwindow">' + value + '</span>');
					}
				}
			}
			strings.push("</div>");
			if (instance.isGroupHeader()) {
				var children = instance.summaryLinks();
				for (var i = 0, n = children.length; i < n; i++) {
					children[i].serialize({
						context: context
					});
				}
			}
			return context;
		};
		
		/**
		* The $$reflective api does not support older browsers...
		*/
		var $$reflective;
		instance.$$reflective = function get$$reflective() {
			return ($$reflective = $$reflective || generate$$reflective({ instance: instance, properties: properties }));
		};
	};

	var $$ReflectiveReferences = [];
	/**
	* @constructor
	*/
	function Reflective(params)
	{
		var instance = this;
		if(!(instance instanceof Reflective))
		{
			throw err("not called as a constructor.");
		}
		params = params || {};
		instance.instance = params.instance;
		try
		{
			Object.defineProperty(instance, "valueOf", {
				get:function getValueOf() {
					var result = $$ReflectiveReferences.length;
					if(!result)
					{
						window.setTimeout(function removeAllReferences(){
							$$ReflectiveReferences.length=0;
							result = 0;
						}, 0);
					}
					$$ReflectiveReferences.push(instance);
					result+=2;
					return function valueOf() {
						return result;
					};
				}
			});
		}
		catch(e) {
			err(e && e.message || e);
		}
	}

	function getInstance(value) {
		if(value instanceof Reflective)
		{
			return value.instance;
		}
		return value;
	};

	function generate$$reflective(params) {
		if(!(params && params.instance && params.properties))
		{
			throw err("generate$$reflective called with insufficient arguments");
		}
		var instance = params.instance;
		var target = new Reflective({instance:instance});
		var properties = params.properties;
		try{
			for(var key in properties) {
				var property = properties[key];
				Object.defineProperty(target, key, {
					configurable: false,
					enumerable: true,
					get: createDelegate(property.$$reflective?{instance:property, key: "$$reflective"}:{instance:instance, key:key}, function getProperty() {
						return this.instance[this.key]();
					}),
					set: createDelegate({instance:instance, key:key}, function setProperty(value) {
						if(typeof value === "undefined" && property.optional && typeof this.instance[this.key].clear === "function")
						{
							this.instance[this.key].clear();
						}
						else
						{
							this.instance[this.key](value);
						}
					})
				});
			}
		} catch(e) {
			err(e && e.message || e);
		}
		return target;
	}


	/**
	* Helper to easily define properties in a constructor and then generate the accessors using this function.
	*/
	function generateAccessors(params){
		if(!(params && params.instance && params.properties))
		{
			throw err("generateAccessors called with insufficient arguments");
		}

		var instance = params.instance;
		var properties = params.properties;

		for (var key in properties) {
			instance[key] = createDelegate(key, function accessor() {
				var key = this;
				var property = properties[key];

				if (!property) {
					throw err("Internal error while accessing property " + key);
				}

				var get = function fGet() {
					var val = property.value;
					if (typeof property.accessor === "function") {
						val = property.accessor();
					}
					if (typeof property.getter === "function") {
						val = property.getter(val);
					}
					if (Object.prototype.toString.apply(val) === '[object Array]') {
						val = val.slice(0); //Return copy of array in stead of reference to the orignal.
					}
					return val;
				};
				var result;

				if (arguments.length > 0) { //Accessor called as setter:
					var value = arguments[0];
					if (typeof property.validator === "function") {
						if (!property.validator(value)) {
							throw err("Failed attempt to set invalid value to " + key);
						}
					}

					var set = function fSet(val) {
						if (typeof property.accessor === "function") {
							return property.accessor(val);
						}
						else {
							return property.value = val;
						}
					};
					switch (typeof property.type === "string" ? property.type.toLowerCase() : "") {
						case "readonly": //do not set value on readonly property
							break;
						case "any":
							result = set(value); //Do not check value on type "any", custom accessors can enhance this type.
							break;
						case "number":
							if (typeof value != property.type) {
								throw err("Property " + key + " requires to be of type " + property.type);
							}
							if (window.isNaN(value))
							{
								throw err("Property " + key + " does not accept NaN (Not a Number) as a value (the value needs to be a number)");
							}
							if (typeof property.max === "number") {
								result = set(Math.min(property.value, property.max));
							}
							else {
								result = set(value);
							}
							break;
						case "string":
							if (typeof value != property.type) {
								throw err("Property " + key + " requires to be of type " + property.type);
							}
							if (typeof property.max === "number") {
								result = set(value.substr(0, Math.min(value.length, property.max)));
							}
							else {
								result = set(value);
							}
							break;
						default:
							if (typeof value != property.type) {
								throw err("Property " + key + " requires to be of type " + property.type);
							}
							result = set(value);
							break;
					}
				}

				return result || get();
			});
			
			if(properties[key].$$reflective)
			{
				properties[key].$$reflective.reference = instance[key];
			}
			
			if (properties[key].optional) {
				instance[key].clear = createDelegate(key, function clearValue() {
					properties[this].value = undefined;
				});
			}
			if(properties[key].properties)
			{
				for (var name in properties[key].properties)
				{
					var aboutProperty = properties[key].properties[name];
					if (typeof aboutProperty === "function") {
						instance[key][name] = createDelegate(instance[key], aboutProperty);
					}
					else {
						instance[key][name] = aboutProperty;
					}
				}
			}
		}
	};

	/**
	 * To manage events (for situations like listeners that register new listeners while being triggered)
	 * @constructor
	 */
	function EventManager(params)
	{
		var instance = this;
		if(!(instance instanceof EventManager))
		{
			throw err("Not called as a constructor.")
		}
		params = params || {};
		
		var key = params.key;
		var context = params.context;
		
		var nested = 0;
		var listeners = [];
		var delayed = [];
		var processing = [];
		var queue = [];
		
		instance.add = function addListener(params)
		{
			params = typeof params === "object" ? params : { listener: params };
			if(!(params && typeof params.listener === "function"))
			{
				throw err("insufficient arguments.");
			}
			var listener = params.listener;
			if(nested && !params.nested)
			{
				delayed.push(listener);
			}
			else
			{
				listeners.push(listener);
			}
		};
		
		instance.forget = function forgetListner(params)
		{
			params = typeof params === "object" ? params : { listener: params };
			if(!(params && typeof params.listener === "function"))
			{
				throw err("insufficient arguments.");
			}
			var listener = params.listener;
			for(var i = Math.max(Math.max(listeners.length, processing.length), delayed.length)-1; i>=0; i--)
			{
				if(listeners[i] === listener)
				{
					listeners.splice(i, 1);
				}
				if(delayed[i] === listener)
				{
					delayed.splice(i, 1);
				}
				if(processing[i] === listener)
				{
					processing.splice(i, 1);
				}
			}
		};
		
		instance.fire = function fireEvent(params) {
			params = params || {};
			
			var isInternal = key === params.key;
			
			nested++;
			if(nested === 1)
			{
				while(listeners.length)
				{
					var listener = listeners.shift();
					processing.push(listener);
					try
					{
						listener.call(context, params.arg || {
							isInternalCall: isInternal, 
							params: params.params
						});
					}
					catch(e)
					{
						try
						{
							err(e&&e.message||e);
						}
						catch(e){}
					}
					
				}
				listeners.push.apply(listeners, processing);
				listeners.push.apply(listeners, delayed);
				processing.length = 0;
				delayed.length = 0;
			}
			else if(params.nested)
			{
				//If an event is fired while the same event is being handled, first the current event is finished.
				// After the current event was finished, the next trigger from the queue will be handled.
				//Be carefull: if a handler always fires it's own event, this will cause for an infinite chain of events.
				queue.push(params);
			}
			nested--;
			
			if((!nested) && queue.length > 0)
			{
				instance.fire(queue.shift());
			}
		};
	}
		
	/**
	* Returns server relative url from full url specified in params {url: window.location.href, empty: '/' }
	*/
	function getServerRelativeUrlFromUrl(params) {
		if (!(params && params.url)) {
			throw err("getServerRelativeUrlFromUrl called with insufficient arguments");
		}
		var url = params.url;
		var empty = params.empty || "";

		var protocolCheck = "://";
		var endOfProtocol = url.indexOf(protocolCheck);

		if (endOfProtocol >= 0) {
			var beginOfRelative = url.indexOf("/", endOfProtocol + protocolCheck.length);
			if (beginOfRelative >= 0) {
				url = url.substr(beginOfRelative);
			}
			else {
				url = empty; //No relative url, use specified string for this scenario.
			}
		}

		return url;
	};
		
	/**
	* Much like jQuery closest function, but searching down the DOM tree, in stead of up.
	* params:
		current: DOM Element or jQuery element object from local jQuery instance (instanceof would not detect otherwise)
		selector: jQuery selector to find closest descendant.
		includeSelf: optional boolean, if true, also test if self is a match.
	*/
	function closestDescendant(params) {
		if (!(params && params.current && params.selector)) {
			throw err("closestDescendant called with in sufficient arguments");
		}
		if(!has$)
		{
			throw err("closestDescenant requires jQuery, use breadthFirst otherwise.");
		}
		
		var current = params.current instanceof $ ? params.current : $(params.current);
		var selector = params.selector;
		var includeSelf = !!params.includeSelf;

		var searchContext = includeSelf ? current : current.children();
		var result = searchContext.filter(selector);
		if (result.length === 0) {
			var nextDepth = searchContext.children();
			if (nextDepth.length > 0) {
				result = closestDescendant({
					current: nextDepth,
					selector: selector,
					includeSelf: includeSelf
				});
			}
		}
		return result.first();
	};

	//Same as Function.CreateDelegate in SharePoint, but I use this module on my blog too.
	function createDelegate(context, handler)
	{
		return function wrapper() {
			return handler.apply(context, arguments);
		};
	}

	//Check if provided string would be a valid Guid.
	function isValidGuid(text)
	{
		if(!(typeof text === "string"))
		{
			return false;
		}
		var lastIndex = text.length-1;
		if(text[0] === '{' && text[lastIndex] === "}")
		{
			text = text.substring(1, lastIndex);
		}
		return typeof text === "string" && text.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
	}

	/**
	* Create random Guid (not using SP.Guid to be able to use this module without SP libraries available, like in my blog).
	* @param {Object=} params OPTIONAL: {braces}
	*/
	function newGuid(params)
	{
		params = params || {};
		function get4Hex()
		{
			return ("0000"+Math.floor((Math.random()*0x10000)).toString(16)).slice(-4);
		}
		function get3Hex()
		{
			return ("000"+Math.floor((Math.random()*0x1000)).toString(16)).slice(-3);
		}
		var prefix1 = Math.floor(Math.random()*5)+1;
		var prefix2 = (Math.floor(Math.random()*4)+8).toString(16);
		var result = get4Hex()+get4Hex()+'-'+get4Hex()+'-'+prefix1+get3Hex()+'-'+prefix2+get3Hex()+'-'+get4Hex()+get4Hex()+get4Hex();

		if(params.braces)
		{
			result = '{' + result + '}';
		}
		return result;
	}

	function escapeTextForXMLInsertion(params) {
		params = params || {};
		var text = "" + params.text;

		//Characters escaped by this function:
		var entityMap = {
			"&": "&amp;",
			"<": "&lt;",
			">": "&gt;",
			'"': '&quot;',
			"'": '&apos;'
		};

		return text.replace(/[&<>"']/g, function escapeCharacter(character) {
			return entityMap[character];
		});
	};

	function escapeTextForHTMLInsertion(params) {
		params = params || {};
		var text = "" + params.text;
		var repeat = !!params.repeat; //NOTE: for mixed quotes in attributes, use javascript escape in stead of html escape! (element.innerHTML = '<div data-example="{\'it\\\'s\'}"></div>'; HTML escape &#39; in stead of \\\' can throw error.)

		//Characters escaped by this function:
		var entityMap = {
			"&": "amp;",
			"<": "lt;",
			">": "gt;",
			'"': 'quot;',
			"'": '#39;',
			"/": '#x2F;'
		};

		var result = text.replace(/[&<>"'\/]/g, function escapeCharacter(character) {
			return [repeat ? '&amp;' : '&', entityMap[character]].join('');
		});
		return result;
	};

	function getIndex(params)
	{
		if(!(params && params.collection && params.instance))
		{
			throw err("getIndex called with insufficient arguments.");
		}
		var collection = params.collection;
		var instance = params.instance;
		
		if(typeof collection.indexOf === "function")
		{
			return collection.indexOf(instance);
		}
		for(var i = 0, n = collection.length; i<n; i++)
		{
			if(collection[i] === instance)
			{
				return i;
			}
		}
		return -1;
	}

	//jquery wrap start
	function depthFirst(params)
	{
		if(!(params && params.element && params.name))
		{
			throw err("depthFirst called with insufficient arguments.");
		}
		var current = params.element;
		var name = params.name;
		
		if(has$)
		{
			return $(current).find(name).first()[0];
		}
		
		for(var i = 0, n = current.childNodes && current.childNodes.length || 0; i < n; i++)
		{
			var child = current.childNodes[i];
			if(getNodeName({element:child, toLowerCase: true}) == name)
			{
				return child;
			}
			var inDepth = depthFirst({element: child, name: name});
			if(inDepth)
			{
				return inDepth;
			}
		}		
	}

	function breadthFirst(params)
	{
		if (!(params && params.element && params.name)) {
			throw err("breadthFirst called with in sufficient arguments");
		}
		var current = params.element;
		var name = params.name;
		
		if(has$)
		{
			return closestDescendant({current: current, selector: name, includeSelf: false})[0];
		}
		var collections = [current.childNodes];
		for(var index = 0; index<collections.length; index++)
		{
			var collection = collections[index];
			
			for(var i = 0, n = collection && collection.length || 0; i < n; i++)
			{
				current = collection[i];
				if(getNodeName({element:current, toLowerCase: true}) == name)
				{
					return current;
				}
				collections.push(current.childNodes);
			}
		}
	}

	function makeElement(params)
	{
		if(!(params && params.name && params.html))
		{
			throw err("makeElement called with insufficient arguments.");
		}
		if(has$)
		{
			return $("<"+params.name+"/>").html(params.html)[0];
		}
		var element = window.document.createElement(params.name);
		element.innerHTML = params.html;
		return element;
	}

	function getNodeName(params)
	{
		if(!(params && params.element))
		{
			throw err("getNodeName called with insufficient arguments.");
		}
		var name = params.element.nodeName || "";
		if(params.toLowerCase)
		{
			name = name.toLowerCase();
		}
		return name;
	}

	function getNodeText(params)
	{
		if(!(params && params.element))
		{
			throw err("getNodeText called with insufficient arguments.");
		}
		var current = params.element;
		return current && (current.textContent || current.innerText) || "";
	}

	function findChildByName(params)
	{
		if(!(params && params.element && params.name))
		{
			throw err("findChildByName called with insufficient arguments.");
		}
		var current = params.element;
		var name = params.name;
		
		if(has$)
		{
			return $(current).children(name + ":first")[0];
		}
		
		var children = current.childNodes;
		var childCount = children && children.length || 0;
		for(var i = 0; i<childCount; i++)
		{
			var child = children[i];
			if(getNodeName({element:child, toLowerCase: true}) == name)
			{
				return child;
			}
		}
	}

	function nextSibblingByName(params)
	{
		if(!(params && params.element && params.name))
		{
			throw err("nextSibblingByName called with insufficient arguments.");
		}
		var current = params.element;
		var name = params.name;
		
		if(has$)
		{
			return $(current).nextAll(name).first()[0];
		}
		
		while(current = current.nextSibling)
		{
			if(getNodeName({element:current, toLowerCase: true}) == name)
			{
				return current;
			}
		}
	}

	function findAll(params)
	{
		if(!(params && params.element && params.name))
		{
			throw err("findAll called with insufficient arguments.");
		}
		if(has$)
		{
			return $(params.element).find(params.name);
		}
		
		var result = params.result = params.result || [];
		var current = params.element;
		var name = params.name;
		if(getNodeName({element:current, toLowerCase: true}) == name && !params.exclusive)
		{
			result.push(current);
		}
		var children = current.childNodes;
		var childCount = children && children.length || 0;
		for(var i = 0; i<childCount; i++)
		{
			findAll({element:children[i], name: name, result: result});
		}
		return result;
	}

	function getAttributeValue(params)
	{
		if(!(params && params.element && params.name))
		{
			throw err("getAttributeValue called with insufficient arguments.");
		}
		if(has$)
		{
			return $(params.element).attr(params.name);
		}
		return params.element.getAttribute && params.element.getAttribute(params.name) || "";
	}

	//The original module I wrote was part of a solution, there I utilised Script on Demand for loading jQuery.
	// Here I do some shady stuff because I wanted to isolate the code I made public from dependencies to other libraries.
	// jQuery is still a dependency (for now)
	var has$ = false;
	var $;
	ns.SummaryLinkStore.setJQuery = function setJQuery(jQuery){
		if(typeof jQuery != "function")
		{
			throw err("setJQuery needs jQuery as an argument.");
		}
		$ = jQuery;
		has$ = true;
	};

	//$$reflective API for modern browsers
	try
	{
		Object.defineProperty(ns, "sls", {
			configurable: false,
			enumerable: true,
			get: function getSls() {
				return _sls.$$reflective();
			},
			set: function setSls(value) {
				_sls = new ns.SummaryLinkStore();
				_sls.deserialize({
					replace: true,
					value: value
				});
			}
		});
		var _sls = new ns.SummaryLinkStore();
	}
	catch(e)
	{
		err(e && e.message || e);
	}

	if(typeof window.RegisterModuleInit === "function")
	{
		window.RegisterModuleInit("oPB.SummaryLinkStore.js", function(){});
	}

	if(typeof window.NotifyScriptLoadedAndExecuteWaitingJobs === "function")
	{
		window.NotifyScriptLoadedAndExecuteWaitingJobs("oPB.SummaryLinkStore.js");
	}
}(window, void window.undefined));

/** @preserve Copyright 2015 VX Company and Pieter-Bart van Splunter (https://summarylinkstore.codeplex.com/license) */
